{
 "cells": [
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "#Android录音app"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "在上一章，我们简要介绍过Kivy要实现跨平台应用，可能在不同的平台需要选择不同的代码，为一些用户增强体验效果，实现具体平台的任务。\n",
    "\n",
    "有时，这些都很简单；比如，如果Kivy发现目标系统支持它，多点触控就会启动——不需要写任何代码，但是要考虑一些点击事件原来的功能可能会与多点触控产生冲突。\n",
    "\n",
    "<!-- TEASER_END-->"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "另外一些平台相关的任务，像代码不能在其他系统上运行，是有很多原因造成的。还记得画图app的鼠标光标吗？代码要用Pygame封装的底层SDL光标功能，如果你熟悉SDL和Pygame那就很简单。因此，为了让app可以跨平台，我们要尽量避免在系统兼容性不好的代码；因为那样可能导致程序崩溃。\n",
    "\n",
    "然而，Kivy应用具有良好的平台兼容性——Mac，Windows，Linux，iOS，Android和Raspberry Pi——都没什么大问题。\n",
    "![](kbpic/3.1Kivysupportsplatforms.png)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "教学大纲：\n",
    "\n",
    "- 通过Pyjnius实现Python与Java的交互\n",
    "- 在Android系统设备上测试Kivy应用\n",
    "- 用Python调用Android的声音API，允许我们记录和播放声频文件\n",
    "- 制作一个紧凑型用户界面，类似Windows Phone\n",
    "- 用图标字体改进app矢量图标的显示"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "##平台相关代码"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "这本书绝大多数app都是平台无关代码，因为Kivy具有高度移植性。但这一次我们做一个仅支持Android平台的应用。这么做肯定会减少我们的用户，但是它能让我们接触到一些具体平台功能的处理方法。\n",
    "\n",
    "这种需求可以实现，是立足于Kivy不断努力支持多个平台，使得用户在不同平台上具有类似的体验。因此，我们可以真正简单的做到一次编写，处处运行。\n",
    "\n",
    "但是，要实现跨平台，你就要用每个系统支持的功能。不同系统功能的最大公约数集合包括屏幕可以显示图像，如果有声卡就获取声音，接受用户的输入等等。\n",
    "\n",
    "每个Kivy应用，本质上都基于Python，还支持Python的标准模块。可以利用网络编程，支持大量的协议操作，还提供很多通用性的算法和功能。\n",
    "\n",
    "还有就是在大多数平台上，纯kivy程序的IO能力会受到限制，通用计算机系统的一小部分都是这样，像智能手机和平板电脑。\n",
    "\n",
    "让我们看看现代移动设备的API接口，这里以Android为例。我们把每个API分成两部分：一部分是Python/Kivy支持的，另一部分不是。\n",
    "\n",
    "Python/Kivy支持的特性如下：\n",
    "\n",
    "- 图形硬件加速\n",
    "- 支持多点触控输入\n",
    "- 播放声音\n",
    "- 支持网络\n",
    "\n",
    "Python/Kivy不支持的特性如下：\n",
    "\n",
    "- 调制解调器，语言电话和短信\n",
    "- 内置摄像头拍照和录像\n",
    "- 内置麦克录音\n",
    "- 数据云存储\n",
    "- 蓝牙和其他近场通信\n",
    "- 位置服务和GPS\n",
    "- 指纹识别\n",
    "- 传感器类，加速器、陀螺仪\n",
    "- 屏幕亮度调节\n",
    "- 振动功能\n",
    "- 电池充电百分比\n",
    ">这些不支持的列表里面，不同的Python模块已经支持，像Audiostream可以录音，Plyer可以实现很多功能。\n",
    ">因此，这些特性并非完全不能支持；实际上，这些功能在不同的平台上都是十分碎片化的，即使在Android系统上也没有统一的版本；因此，你写完具体平台的代码后，还是会发现没法儿移植。\n",
    "\n",
    "从前面的比较中可以看出，Android有一堆功能，只要一部分被Python/Kivy支持。这无疑为你用Kivy开发Android应用留下了大量的自由想象空间。你会学到Python调用Android API的知识，可以让Kivy做任何事情。\n",
    "\n",
    "另一个优势就是，你可以编写全新的类去支持具体特定硬件的移动设备，包括虚拟现实app，支持的陀螺仪游戏，全景拍摄相机等等。"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "collapsed": true
   },
   "source": [
    "###Pyjnius介绍"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "collapsed": true
   },
   "source": [
    "要充分利用Android功能，就要用Java写的一堆API。我们要做的录音app，类似于Android和iOS的应用，很简单的功能。不像纯Kivy程序要从头开始，Android API为我们提供一堆录音的功能。\n",
    "\n",
    "下面我们就通过做录音app来演示Python-Java交互的[Pyjnius](https://github.com/kivy/pyjnius)模块卓越的性能，同样是Kivy开发者的项目。我们要开发的内容很简单——录音，回放功能——你会发现这种交互很简单，不需要一堆错综复杂的细节去实现这点小功能。\n",
    "\n",
    "Pyjnius最有趣的属性就是它并非在Android上面添加一个层来调用API，而是运行你直接通过Python运行Java。这样你就可以完全使用原生的Android API，可以参考适合Java开发的Android文档，不过不是Python文档。但是，这比没有API文档要好。\n",
    ">我们这里说Pyjnius是用来做Android开发的，其实也可以开发Java桌面应用。这是很有趣的，因为还有一个Java API的Python模块叫Jython，很慢而且不完整。Pyjnius可以让你直接使用CPython，再加上Numpy就可以让程序飞起来。\n",
    ">总之，让你想通过Python用Java，考虑Pyjnius吧。"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "collapsed": true
   },
   "source": [
    "###Android模拟器"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "这章做的app是要运行在Android上的，不能运行在我们的电脑上，因此我们需要用到Android设备，如果你没有设备，也可以安装Android模拟器。一个方便高效的Android模拟器可以让你事半功倍。\n",
    "\n",
    "推荐一个模拟器，就是[Genymotion](http://www.genymotion.com/)，你可以下载一个免费版来用。不同的系统安装方法不同，我们就不提供教程了，自行谷歌之，还是比较简单的。\n",
    "\n",
    "用虚拟机安装Android模拟器的时候，下面一些建议供参考：\n",
    "\n",
    "- 建议保持Android最新版本，向后兼容性比较差；旧版本的系统级别的调试问题没有完全解决。\n",
    "- Android社区资源丰富，如果有问题就检索，你遇到的坑别人也踩过。\n",
    "- Kivy Launcher app是很不错的测试工具，你可以在[官方网站](http://kivy.org/)找到apk，建议装到手机上，方便程序调试。\n",
    "- 不同的模拟器质量和兼容性层次不齐。如果你发现一次没搞定，建议你换个虚拟机或模拟器试试。\n",
    "\n",
    "下面这个截图就是Genymotion启动的模拟器，完全支持Kivy Launcher。\n",
    "![Genymotion](kbpic/3.2AndroidGenymotion.png)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "collapsed": true
   },
   "source": [
    "##Metro UI"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "collapsed": true
   },
   "source": [
    "现在让我们用Window Phone的主屏风格来建立一个用户界面。这些不同大小的矩形彩色网格，被称为**Metro UI**风格，不过后来更名为**Modern UI**。我们的app就是要仿这个。\n",
    "![metroui](kbpic/3.3metroui.png)\n",
    "\n",
    "当然，我们并不是要做出这样，只是用一下风格来构建我们的界面。下面是对风格的总结：\n",
    "\n",
    "- 每个元素都是一个矩形网格\n",
    "- IU元素呈现扁平化特征（第一章讨论过，表面纯色，没有阴影，也没有圆角）\n",
    "- 格子可以根据需要变大，方便点击\n",
    "\n",
    "看起来非常简单吧。其实用Kivy实现起来也很简单。"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "collapsed": true
   },
   "source": [
    "###按钮"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "collapsed": true
   },
   "source": [
    "现在开始吧，首先做个按钮`Button`类，就像我们在之前的应用里做的,这里我们重用第二章画图app的按钮：\n",
    "```yaml\n",
    "<Button>:\n",
    "    background_normal: 'button_normal.png'\n",
    "    background_down: 'button_down.png'\n",
    "    background_color: C('#95A5A6')\n",
    "    font_size: 40\n",
    "```\n",
    "我们之前设计调色板时，把背景色成设置重白色。这里我们的`background_color `属性是一个底色，我们分配一个浅白色作为`background_color`。这次我们不要边框有颜色。\n",
    "\n",
    "之后就是按下按钮的颜色，`background_down`属性我们设置成25%透明白色。再在上面加上黑色，就可以获得一个深色的背景：\n",
    "![backgroundcolor](kbpic/3.4backgroundcolor.png)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "###网格结构"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "布局有点小复杂。Kivy没有现成的Modern UI模板可用，我们要自己仿制一个`GridLayout`部件。它和`BoxLayout`部件类似，不过是二维的，所以没有`orientation: 'horizontal'`或`'vertical'`属性。\n",
    "\n",
    "如果没有其他需求，一个`GridLayout`部件就可以搞定，但是我们还想要不同尺寸的按钮。目前，`GridLayout`部件不支持通过网格的合并成更大的网格（HTML里面的`rowspan`和`colspan`属性更方便）。因此，我们换个思路，先用一个`GridLayout`部件作为根部件，用大网格，然后再在里面增加一个`GridLayout`部件，用小网格。\n",
    "\n",
    "因为Kivy可以完美支持嵌套布局，我们可以用下面的kv代码实现`recorder.kv`文件：\n",
    "\n",
    "```yaml\n",
    "#:import C kivy.utils.get_color_from_hex\n",
    "\n",
    "GridLayout:\n",
    "    padding: 15\n",
    "    \n",
    "    Button:\n",
    "    background_color: C('#3498DB')\n",
    "    text: 'aaa'\n",
    "    \n",
    "    GridLayout:\n",
    "        Button:\n",
    "        background_color: C('#2ECC71')\n",
    "        text: 'bbb1'\n",
    "        \n",
    "        Button:\n",
    "        background_color: C('#1ABC9C')\n",
    "        text: 'bbb2'\n",
    "        \n",
    "        Button:\n",
    "        background_color: C('#27AE60')\n",
    "        text: 'bbb3'\n",
    "        \n",
    "        Button:\n",
    "        background_color: C('#16A085')\n",
    "        text: 'bbb4'\n",
    "        \n",
    "    Button:\n",
    "    background_color: C('#E74C3C')\n",
    "    text: 'ccc'\n",
    "        \n",
    "    Button:\n",
    "    background_color: C('#95A5A6')\n",
    "    text: 'ddd'\n",
    "```\n",
    "翻翻前面的章节，把`main.py`也做出来就可以运行了，自己试试吧。注意类的名称是`RecorderApp`，kv文件名大写，再加`App`，不清楚参考第一章命名相关内容。\n",
    "\n",
    "注意嵌套的`GridLayout`部件是怎么和同级的大按钮放在一起的。如果你记得前面那个WP系统主平面，还得继续改进代码：四个小按钮和一个大按钮一样大。嵌套的`GridLayout`部件是这些小按钮的容器。"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "####可视化属性"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "在外面的网格里，`padding`属性是为了让部件离屏幕的四边有一定边距。把其他`GridLayout`部件的可视化属性都放到一个类中：\n",
    "```yaml\n",
    "    <GridLayout>:\n",
    "    cols: 2\n",
    "    spacing: 10\n",
    "    row_default_height: \n",
    "        (0.5 * (self.width - self.spacing[0]) -\n",
    "        self.padding[0])\n",
    "    row_force_default: True\n",
    "```\n",
    ">要注意的是`padding`和`spacing`属性都是列表，不是数值。`spacing[0]`是水平间距，之后是垂直间距。但是，我们用一个数值初始化，就像前面的代码显示的；数值可以用在两个方向。\n",
    "\n",
    "嵌套网格由具有相同间距的两列组成，`row_default_height`属性是厚度：我们可以说，“让行高等于格子宽度”。但是下面我们手动计算想要的高度，0.5是因为有两列：\n",
    "\n",
    "$$行高 = 0.5 × (屏幕宽度 - 所有边距和间距)$$\n",
    "\n",
    "如果我们不这么做，网格里面的按钮就会垂直填充全部空间，这不是我们要的效果，尤其不是很多按钮的时候，每个就会显得巨大难看。另外，我们希望所有的按钮都是方块。\n",
    "\n",
    "下面就是之前代码设计的“Modern UI”界面：\n",
    "![uidesign](kbpic/3.5uidesign.png)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "###可缩放的矢量图标"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "要做好看的应用UI，就得有图标，不只是按钮+文字。当然，我们可以在按钮上加图片，但是更好的办法是有图标字体——后面你会发现这种方法更具柔性。"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "####图标字体"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "图标字体本质上和普通字体一样，除了它们的符号与文字无关。比如，你输入“P”就可以获得Python的logo，而不是字母P；每个字都是与字母相对于的图标。\n",
    "\n",
    "使用图标字体的不足——用这些字体的代码很难读——因为文字-图标对应关系不太明显。这点可以通过常量来代替要输入符号。\n",
    "\n",
    "还有一些字体不使用英文字母，改用Unicode码与图标对应，比如[Emoji颜文字](http://en.wikipedia.org/wiki/Emoji)。使用这类图标字体的前提是目标平台支持Unicode，这方面不是每个平台都ok，尤其是移动平台。我们这里的app只用ASCII码。"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "####合理使用图标字体"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "在网页上，字体图标解决了很多图片（[栅格图](http://en.wikipedia.org/wiki/Raster_graphics)）相关的问题：\n",
    "\n",
    "- 首先是图片放大后会失真——虽然有算法可以解决这类问题，但是并不完美。相反图标字体是矢量图，可以无限放大。\n",
    "- 栅格图文件包含的示意图（像图标和UI元素）比矢量格式字节空间大。这就显然不能应用到JPEG格式的图片上，会使字节空间更大。\n",
    "- 另外，图标字体通常就是一套字体放在一个文件上，就是说一个HTTP请求就可以遍历。普通的图片分别是单个文件，明显增加HTTP请求负担；有一些方法可以改善这点，像[CSS sprites](https://css-tricks.com/css-sprites/)可以把多个图片合成一张图改善性能，不过使用不太广泛，而且有些问题。\n",
    "- 图标字体里面，颜色可以随意变化，在CSS文件增加`color: red`即可。尺寸、角度和其他那些普通图片不容易搞定的属性都很容易设置，就好像在位图上操作。\n",
    "\n",
    "以上这些并不适用于Kivy开发，不过图标字体的使用已经是现代网页开发的范本了，尤其是自大量优质字体开始出现以来——现在有几百种图标字体可用。\n",
    "\n",
    ">最棒的两个免费字体就是[Font Squirrel](http://www.fontsquirrel.com)和[Google Fonts](https://www.google.com/fonts)。不用介意这些网站的字体是用来做网页开发的，大多数字体都可以离线使用。\n",
    ">最需要注意的是字体文件格式：Kivy只能支持True Type(`.ttf`)文件格式。好在大多数字体都是这个格式，即使不是也可以格式转换。"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "####Kivy中使用图标字体"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "我们的app使用由John Caserta设计的[Modern Pictograms](http://www.fontsquirrel.com/fonts/modern-pictograms)免费字体。截图如下所示：\n",
    "![ModernPictogramsiconfont](kbpic/3.6ModernPictogramsiconfont.png)\n",
    "\n",
    "要把字体加入Kivy程序，我们可以用第一章时钟app的方法。这里，我们不这么做，因为图标字体字体风格完全不同。还是，通过字体名称连接字体，而不是用字体文件名（`modernpics.ttf`）连接。这样，你就可以重命名字体文件或者移动文件路径，而不用每次都通篇改一遍。在`main.py`写如下代码："
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "collapsed": true
   },
   "outputs": [],
   "source": [
    "from kivy.app import App\n",
    "from kivy.core.text import LabelBase\n",
    "\n",
    "class RecorderApp(App):\n",
    "    pass\n",
    "\n",
    "if __name__ == '__main__':\n",
    "    LabelBase.register(name='Modern Pictograms',\n",
    "                       fn_regular='modernpics.ttf')\n",
    "    RecorderApp().run()"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "这样`recorder.kv`文件就可以使用图标字体了。首先，我们把`Button`改进一下，方便后面改变字体。代码如下：\n",
    "```yaml\n",
    "<Button>:\n",
    "    background_normal: 'button_normal.png'\n",
    "    background_down: 'button_down.png'\n",
    "    font_size: 24\n",
    "    halign: 'center'\n",
    "    markup: True\n",
    "```\n",
    "\n",
    "`halign: 'center'`表示我们希望每行文字都在按钮的正中间。`markup: True`是必须的，因为我们后面自定义按钮需要这样。\n",
    "\n",
    "现在，我们来升级所有按钮。这是一个例子：\n",
    "```yaml\n",
    "Button:\n",
    "    background_color: C('#3498DB')\n",
    "    text:\n",
    "        ('[font=Modern Pictograms][size=120]'\n",
    "        'e[/size][/font]\\nNew recording')\n",
    "```\n",
    "通常不需要为Kivy文件字符串加括号，这么做是为了显示成多行，这和一行长串是一样的。\n",
    "\n",
    "注意在`[font][size]`表情里面的`'e'`，这就是图标代码。每个按钮用一个图标，改变一个图标就是替换`recorder.kv`文件里面的一个字母。具体对应关系可以在Modern Pictograms字体网站查询。\n",
    "\n",
    ">为了方便浏览图标字体，你需要使用字体浏览器。通常，每个系统都有类似程序。\n",
    "- Windows系统使用Character Map\n",
    "- Mac系统使用Font Book\n",
    "- Linux系统由桌面环境决定，GNOME使用gnome-font-viewer\n",
    "\n",
    ">当然也可以网页搜索。流行字体都有详细的说明。\n",
    "\n",
    "下面就是我们的界面啦：\n",
    "![appui](kbpic/3.7appui.png)\n",
    "\n",
    "不错吧，可以Modern UI很像吧。\n",
    ">你可能想知道右上角的小绿图标是干嘛的。它们是为了给不同的设备设置不同的录音质量。另外三个按钮——录音，播放，删除——尚不足以呈现Modern UI的风格，因为它需要更丰富好玩的外观。"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "##在Android上测试"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "collapsed": true
   },
   "source": [
    "现在，我们的app虽然没有包含任何Android平台相关的代码，但是我们也可以在Android平台上测试。能这么做的前提是Android平台上装了Kivy Launcher。\n",
    "\n",
    "把app打包成Kivy Launcher支持的程序有点琐碎。我们打算增加两个文件，`android.txt`和`icon.png`，都放在`main.py`与`recorder.kv`同名文件夹里，然后拷贝到SD卡的`/Kivy`文件夹即可，如下图所示：\n",
    "![SDcard](kbpic/3.8SDcard.png)\n",
    "当然启动Kivy Launcher，它就会扫描文件目录的完整路径，如果没SD卡还是能找到。\n",
    "\n",
    "`android.txt`非常简单：\n",
    "\n",
    "```\n",
    "title=App Name\n",
    "author=Your Name\n",
    "orientation=portrait\n",
    "```\n",
    "`title`和`author`设置后会在程序列表显示出来。`orientation`可以是`portrait`（正常显示，高度>宽度）和`landscape`（水平显示，宽度>高度），由应用布局设计决定。\n",
    "\n",
    "`icon.png`文件是可选的，如果没有就是空白图标。建议找个漂亮的，程序的第一印象就是图标。注意`icon.png`文件名不能改变，否则Kivy Launcher找不到启动位置。\n",
    "\n",
    "把之前做过的程序都放进去，你就会看到程序列表，如下所示：\n",
    "![applicationslist](kbpic/3.9applicationslist.png)\n",
    ">如果不是这样，建议检查一下文件路径。\n",
    "\n",
    "现在就可打开程序了。这是在Android上测试Kivy程序最方便的办法——就是简单的复制文件。"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "collapsed": true
   },
   "source": [
    "##用Android的API"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "已经完成了app的用户界面，下面我们来用Android的API实现录音的功能，要用到的两个Java类是`MediaRecorder`和`MediaPlayer`。\n",
    "\n",
    "Python和Java都是面向对象语言，看着好像差不多，但是两者对OOP理论的应用大相径庭。与Python相比，很多Java的API都在一坨坨的使用设计模式。所以，你在解决很小的任务时，也需要写一堆废话，就不用觉得奇怪。\n",
    ">在1913年的时候，Vladimir Lenin就写下了对Java架构的评论：\n",
    "\n",
    ">*只有一种办法可以粉碎这些“类”的阻力，那就是，在我们的社会中找到能够推陈出新的力量。*\n",
    "\n",
    ">这段话没有提及之后的Python和Pyjnius，但是观点明确——即使在一百年前，过度使用类也是不受社会欢迎的。\n",
    "\n",
    "幸运的是，我们的任何相对简单。要用Android API实现录音，我们只需要下面5个Java类：\n",
    "\n",
    "- `android.os.Environment`：这个类提供了很多有用的环境变量。我们需要用它来确定文件保存的路径。临时存放的位置就是`'/sdcard/'`或者简单的内容，但是即使不同的设备路径不一样。因此，我们别这么设置路径。\n",
    "- `android.media.MediaRecorder`：这是我们的主力，实现了音频和视频的抓取和保存功能。\n",
    "- `android.media.MediaRecorder$AudioSource`， `android.media.MediaRecorder$AudioEncoder`和`android.media.MediaRecorder$OutputFormat`：这些类列出了我们需要传递到`MediaRecorder`不同方法里面的参数。\n",
    "\n",
    ">Java类命名规则：\n",
    ">通常类名里面的`'$'`符号表示内部类。这种方法有点无厘头，因为你可以声明一个相似的类不用后面跟任何东西——`'$'`在Java变量和类名称中是可用的，与JavaScript里面的没有太多不同。但这种奇葩的命名方法让人无语。"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "###加载Java类"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "下面的代码就是通过Pyjnius加载Java类："
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 2,
   "metadata": {
    "collapsed": false
   },
   "outputs": [],
   "source": [
    "from jnius import autoclass\n",
    "\n",
    "Environment = autoclass('android.os.Environment')\n",
    "MediaRecorder = autoclass('android.media.MediaRecorder')\n",
    "AudioSource = autoclass('android.media.MediaRecorder$AudioSource')\n",
    "OutputFormat = autoclass('android.media.MediaRecorder$OutputFormat')\n",
    "AudioEncoder = autoclass('android.media.MediaRecorder$AudioEncoder')"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "如果你直接运行代码，可能会出现下面的错误：\n",
    "\n",
    "- **ImportError: No module named jnius**：如果Pyjnius没装\n",
    "- **jnius.JavaException: Class not found 'android/os/Environment'**：如果需要Android类没加载成功（在桌面系统上Android没装好可能会这样）。\n",
    "\n",
    "如果遇到其中任何一个错误都说明没配置好。因为代码不再是跨平台的了，让我们到Android设备或者模拟器上测试一下。这个app完全依赖Android相关的Java特性。\n",
    "\n",
    "下面我们将把Java类与Python代码结合到一起。\n",
    ">记得这些类的定义文档都是Java的，不是Python。你可以看看Google官方的[Android开发入门](http://developer.android.com/reference/packages.html)，然后把Java反应成Python代码，看着很恐怖，多试试，其实很简单。"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "collapsed": true
   },
   "source": [
    "###查找保持路径"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "让我们演示一下Pyjnius这种混合语言使用API的过程。我们还可以通过Java检查SD卡是否已经挂载。\n",
    "\n",
    "```java\n",
    "import android.os.Environment;\n",
    "String path = Environment.getExternalStorageDirectory().getAbsolutePath();\n",
    "```\n",
    "翻译成Python就是："
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "collapsed": true
   },
   "outputs": [],
   "source": [
    "Environment = autoclass('android.os.Environment')\n",
    "path = Environment.getExternalStorageDirectory().getAbsolutePath()"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "两者是完全一样的。然后，我们可以通过log查看运行的结果。"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "collapsed": true
   },
   "outputs": [],
   "source": [
    "from kivy.logger import Logger\n",
    "Logger.info('App: storage path == \"%s\"' % path)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "调试信息会显示下面这行日志：\n",
    "\n",
    "**[INFO] App: storage path == \"/storage/sdcard0\"**"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "####在设备中看日志"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "collapsed": true
   },
   "source": [
    "当你在开发阶段运行Kivy应用时，日志会立刻在命令行窗口显示。当你在Kivy Launcher运行代码时，虽然不是很容易，显示日志也是非常有用的特性。\n",
    "\n",
    "要看到日志，你可以在程序运行的时候到程序目录下（`/Kivy/Recorder `），里面会生成一个新的`.kivy`目录，这个目录里面有日志文件`.kivy/logs`。\n",
    "\n",
    "如果是用Android SDK运行模拟器，可以打开开发者模式的USB调试功能，然后用`adb logcat`查看所有日志，里面包括Kivy日志。如果你用Android Studio 或Eclipse等IDE，你可以用logcat对日志进行过滤处理。\n",
    "\n",
    "当调试程序出问题或应用不能启动的时候日志文件是很冗长的。Kivy还打印了各种关于运行环境的警告日志，像缺少库文件或功能，Python模块加载失败，或者其他提示。"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "###录音"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "现在让我们用Android的API来逐个实现app的功能。现在的代码就是把Android的API翻译成Python。如果你对原始的Java代码感兴趣，你可以去官方网站看[文档](http://developer.android.com/guide/topics/\n",
    "media/audio-capture.html)。\n",
    "\n",
    "下面的代码就是`MediaRecorder`对象初始化："
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "collapsed": true
   },
   "outputs": [],
   "source": [
    "storage_path = (Environment.getExternalStorageDirectory()\n",
    "                .getAbsolutePath() + '/kivy_recording.3gp')\n",
    "\n",
    "recorder = MediaRecorder()\n",
    "\n",
    "def init_recorder():\n",
    "    recorder.setAudioSource(AudioSource.MIC)\n",
    "    recorder.setOutputFormat(OutputFormat.THREE_GPP)\n",
    "    recorder.setAudioEncoder(AudioEncoder.AMR_NB)\n",
    "    recorder.setOutputFile(storage_path)\n",
    "    recorder.prepare()"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "这就是把啰七八嗦的Java代码直译成Python的代码。\n",
    "\n",
    "你可以自定义里面的属性。比如`AMR_NB`（**自适应多速率，Adaptive Multi-Rate**编解码器，用来做语音优化的，广泛用于GSM和其他移动网络的设备中）改成`AudioEncoder.AAC`（**进阶音讯编码，Advanced Audio\n",
    "Coding**标准，类似于MP3的一种编码标准）。这么改不是很合适，因为手机的麦克风不只是录制音乐，还要录制你的声音。\n",
    "\n",
    "下面就是“开始/结束录音(Begin/End)”按钮部分代码。这部分代码和第一章时钟app的思路一样："
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "collapsed": true
   },
   "outputs": [],
   "source": [
    "class RecorderApp(App):\n",
    "    is_recording = False\n",
    "    \n",
    "    def begin_end_recording(self):\n",
    "        if (self.is_recording):\n",
    "            recorder.stop()\n",
    "            recorder.reset()\n",
    "            self.is_recording = False\n",
    "            self.root.ids.begin_end_recording.text = \\\n",
    "                ('[font=Modern Pictograms][size=120]'\n",
    "                 'e[/size][/font]\\nBegin recording')\n",
    "            return\n",
    "        \n",
    "        init_recorder()\n",
    "        recorder.start()\n",
    "        self.is_recording = True\n",
    "        self.root.ids.begin_end_recording.text = \\\n",
    "            ('[font=Modern Pictograms][size=120]'\n",
    "            '%[/size][/font]\\nEnd recording')"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "很简单吧，就是首先储存状态`is_recording`，然后实现各个功能：\n",
    "\n",
    "1. 开始或停止`MediaRecorder`对象；\n",
    "2. 改变`is_recording`状态；\n",
    "3. 升级按钮文字以反映当前状态。\n",
    "\n",
    "程序的最后一部分就是升级`recorder.kv`，我们要调整一下“开始/结束录音(Begin/End)”按钮代码来调用`begin_end_recording()`函数：\n",
    "```yaml\n",
    "    Button:\n",
    "        id: begin_end_recording\n",
    "        background_color: C('#3498DB')\n",
    "        text:\n",
    "            ('[font=Modern Pictograms][size=120]'\n",
    "            'e[/size][/font]\\nBegin recording')\n",
    "        on_press: app.begin_end_recording()\n",
    "```\n",
    "\n",
    "这就搞定了。现在运行程序就可以向SD卡里录音了。不过在这之前请先看看下节内容。按钮的节目如下所示：\n",
    "\n",
    "![begainendbutton](kbpic/3.10begainendbutton.png)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "####重要警告——权限"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "默认的Kivy应用没有录音权限，`android.permission.RECORD_AUDIO`。如果初始化`MediaRecorder`实例就会失败。\n",
    "\n",
    "当然有很多方法解决这个问题。首先，最容易的就是重新编译Kivy Launcher源代码，让应用获取录音权限，[最新版在这里](https://github.com/mvasilkov/kivy_launcher_hack)。\n",
    "\n",
    "在安装`.apk`文件之前，请卸载原来的版本。另外如果你感受一下Android反编译，也可以用[**apktool](https://code.google.com/p/android-apktool/)直接反编译apk文件。步骤如下：\n",
    "\n",
    "1. 下载Kivy Launcher的app文件`KivyLauncher.apk`，用apktool文件命令：\n",
    "    ```sh\n",
    "    apktool d -b -s -d KivyLauncher.apk KivyLauncher\n",
    "    ```\n",
    "2. 向`AndroidManifest.xml`文件里增加权限：\n",
    "    ```xml\n",
    "    <uses-permission android:name=\"android.permission.RECORD_AUDIO\" />\n",
    "    ```\n",
    "3. 重新打包成`.apk`文件：\n",
    "    ```sh\n",
    "    apktool b KivyLauncher KivyLauncherWithChanges.apk\n",
    "    ```\n",
    "4. 用`jarsigner`工具为`.apk`文件签名。可以看看签名Android包的[官方文档](http://developer.android.com/tools/publishing/app-signing.\n",
    "html#signing-manually)\n",
    "\n",
    "这样，安装Kivy Launcher后就可以录音了。\n",
    ">你可以用同样的方法在通过Pyjnius为Python代码增加不同的权限。比如，要获得GPS API接入的权限，你的app就需要`android.permission.ACCESS_FINE_LOCATION`，其它Android设备权限可以看官方[文档](http://developer.android.com/\n",
    "reference/android/Manifest.permission.html)。"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "collapsed": true
   },
   "source": [
    "###播放声音"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "collapsed": true
   },
   "source": [
    "播放声音很简单，也不需要权限，对应的API也更简练。我们就要一个类`MediaPlayer`："
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "collapsed": true
   },
   "outputs": [],
   "source": [
    "MediaPlayer = autoclass('android.media.MediaPlayer')\n",
    "player = MediaPlayer()"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "collapsed": true
   },
   "source": [
    "当用户按下播放(Play)按钮时，下面的代码就要运行。我们还为下一节的*删除文件*增加了`reset_player()`函数。代码如下："
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "collapsed": true
   },
   "outputs": [],
   "source": [
    "def reset_player():\n",
    "    if (player.isPlaying()):\n",
    "        player.stop()\n",
    "    player.reset()\n",
    "\n",
    "def restart_player():\n",
    "    reset_player()\n",
    "    try:\n",
    "        player.setDataSource(storage_path)\n",
    "        player.prepare()\n",
    "        player.start()\n",
    "    except:\n",
    "        player.reset()"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "这些API的方法定义都可以在官方文档找到，不过意思很简单：就是复位播放器，加载声音文件，开始播放。文件的格式是自动设置的，可以让代码简单点。\n",
    ">实际上这类代码应该一直放在`try ... catch`里面，因为不知道什么地方就会异常。比如文件格式不对，SD卡没插好，或是读取失败，涉及到IO的问题多了。最好的办法就是*小心驶得万年船（better safe than sorry）*。"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "###删除文件"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "collapsed": true
   },
   "source": [
    "最后一个功能是用`java.io.File`，和Android关系不大了，是Java标准库。Android官方文档也会包含Java核心类的解释，尽管Java比Android早几十年。代码很简单，就是最后一行："
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "collapsed": true
   },
   "outputs": [],
   "source": [
    "File = autoclass('java.io.File')\n",
    "class RecorderApp(App):\n",
    "    def delete_file(self):\n",
    "        reset_player()\n",
    "        File(storage_path).delete()"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "首先，我们用`reset_player()`函数停止播放功能，然后把文件删掉。\n",
    "\n",
    "奇葩的是，Java的`File.delete()`方法不会抛出异常，所以不需要用``try ... catch``语句去控制异常了。能省则省！\n",
    "\n",
    ">细心的读者会问为啥不用Python的标准库`os.remove()`呢。其实用Java库跟Python一样简单；不过Java更快一点。另外，可以看出Pyjnius可以很好的使用Java类。\n",
    ">还要注意这个函数在桌面系统上也可以运行，因为它与Android无关；你只要用Java和Pyjnius就可以了。\n",
    "\n",
    "现在UI和三个主要的功能都实现了，我们的app完成了。"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "##总结"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "凡事都有利弊，非移植性代码和可移植性代码都如是。但是，做出正确的选择实际上非常困难，因为选择原生API通常会发生在项目早期，到了后期可能觉得完全不切实际，导致项目终歇菜。\n",
    "\n",
    "本章开篇已经讨论过这种方法的主要优势：对于平台相关的代码，你实际上可以做任何事。没有人为的限制；你的Python代码可以不受限制的接入原生代码的底层API。\n",
    "\n",
    "但是，依赖单一平台的风险就是：\n",
    "\n",
    "- Android市场自然比Android+iOS+...市场小\n",
    "- 把代码移植到新系统，要使用这些平台相关的特性时很困难\n",
    "- 如果项目只在一个平台，如果有平台要求发生改变就很危险。被Google封杀比同时被AppStore和Google，以及其他市场封杀的风险大得多\n",
    "\n",
    "你可以好好想想，做一个适合你的app的决定。"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "##再说UI"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "总之，大胆模仿其他UI模式理念（包括布局，字体，配色等）。毕加索的名言，“杰出艺术家模仿，伟大艺术家盗窃”，这正是当今网页和应用开发的精髓。\n",
    "\n",
    "另外，微软用了“Modern UI”做了一堆应用，包括移动应用和桌面应用，并不是说这种设计模式就是好。众所周知，微软的操作系统才是这种设计模式被接受的基础。\n",
    "\n",
    "现在放下Java吧。下一章我们用Python大名鼎鼎的Twisted网络框架做一个简单的CS模式的聊天app。"
   ]
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "Python 3",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython3",
   "version": "3.5.1"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 0
}
